import { noop } from 'lodash-es'
import { ChannelConnection } from './connection'
import {
  Function_Attr_Once,
  Function_Attr_Raw,
  Field1,
  Field2,
  Field3,
  Field4,
  Field5,
  Field6,
  NoRecursion_Flag,
} from './symbols'
import { EventNames, EventParams, EventsMap, EventResult, DefaultEventsMap } from './types'
export * from './connection'

const getOnceListener = <Fn extends (...args: any[]) => any>(fn: Fn) => {
  return new Proxy(fn, {
    get: (target, p, receiver) => {
      switch (p) {
        case Function_Attr_Once:
          return true
        case Function_Attr_Raw:
          return target
        default:
          return Reflect.get(target, p, receiver)
      }
    },
  })
}

type MaybePromise<T> = T | Promise<T>
type ReservedEventsMap = {
  '*'(): void
  error(err: any): void
  disconnected(connect: ChannelConnection): void
  connected(connect: ChannelConnection): void
}
const reservedEvents = new Set(['error', 'disconnected', 'connected', '*'])

enum EventType {
  Emit,
  Invoke,
  HandleSuccess,
  HandleFailure,
}

const wrapEventPacket = (name: string, type: EventType, id: number, args: any[]) => {
  return [name, type, id, ...args]
}

const unwrapEventPacket = (
  packet: any[]
): {
  name: string
  type: EventType
  id: number
  args: any[]
} => {
  const [name, type, id, ...args] = packet
  return { name, type, id, args }
}

const invokeKey = (name: string, id: number) => {
  return `${name}_${id}`
}

export class ChannelEvent<Name extends string, Args extends any[], Data> {
  constructor(
    public readonly name: Name,
    public readonly args: Args,
    public readonly data?: Promise<Data>
  ) {
  }
}

type ChannelListener<
  This,
  E extends EventsMap = DefaultEventsMap,
  N extends EventNames<E> = EventNames<E>
> = (this: This, ...args: EventParams<E, N>) => any

type ChannelHandler<
  This,
  E extends EventsMap = DefaultEventsMap,
  N extends EventNames<E> = EventNames<E>
> = (this: This, ...args: EventParams<E, N>) => MaybePromise<EventResult<E, N>>

type ChannelEventsMapTransform<E extends EventsMap> = {
  [K in keyof E & string]: E[K] extends (...args: any[]) => any
    ? (e: ChannelEvent<K, Parameters<E[K]>, ReturnType<E[K]>>) => any
    : () => any
}

export interface RemoteChannelEmitter<
  Local extends EventsMap = DefaultEventsMap,
  Remote extends EventsMap = DefaultEventsMap,
  _RegisterHandles extends Record<string, any> = {},
  _Events extends EventsMap = ChannelEventsMapTransform<Omit<Local, keyof ReservedEventsMap>> &
    ReservedEventsMap,
  _Handle extends EventsMap = Omit<Local, keyof ReservedEventsMap> & {
    '*'(name: string, ...args: any[]): any
  },
  _Invoke extends EventsMap = Omit<Remote, keyof ReservedEventsMap>
> {
  addListener<N extends EventNames<_Events>>(
    name: N,
    listener: ChannelListener<this, _Events, N>
  ): this
  on<N extends EventNames<_Events>>(name: N, listener: ChannelListener<this, _Events, N>): this
  once<N extends EventNames<_Events>>(name: N, listener: ChannelListener<this, _Events, N>): this
  removeListener<N extends EventNames<_Events>>(
    name: N,
    listener: ChannelListener<this, _Events, N>
  ): this
  off<N extends EventNames<_Events>>(name: N, listener: ChannelListener<this, _Events, N>): this
  prependListener<N extends EventNames<_Events>>(
    name: N,
    listener: ChannelListener<this, _Events, N>
  ): this
  prependOnceListener<N extends EventNames<_Events>>(
    name: N,
    listener: ChannelListener<this, _Events, N>
  ): this
  removeAllListeners(): this
  removeAllListeners<N extends EventNames<_Events>>(name: N): this
  listeners<N extends EventNames<_Events>>(name: N): ChannelListener<this, _Events, N>[]
  rawListeners<N extends EventNames<_Events>>(name: N): ChannelListener<this, _Events, N>[]
  listenerCount<N extends EventNames<_Events>>(name: N): number
  eventNames(): keyof _Events[]
  handle<N extends EventNames<Omit<_Handle, keyof _RegisterHandles & keyof _Handle>>>(
    name: N,
    handler: ChannelHandler<this, _Handle, N>
  ): RemoteChannelEmitter<Local, Remote, _RegisterHandles & Record<N, any>>
  removeHandler<N extends EventNames<_Handle>>(
    name: N
  ): RemoteChannelEmitter<Local, Remote, Omit<_RegisterHandles, N>>
  getHandler<N extends EventNames<_Handle>>(name: N): ChannelHandler<this, _Handle, N> | undefined
  handlerNames(): (keyof _Handle)[]
  removeAllHandlers(): RemoteChannelEmitter<Local, Remote>
  invoke<N extends EventNames<_Invoke>>(
    name: N,
    ...args: EventParams<_Invoke, N>
  ): Promise<EventResult<_Invoke, N>>
  connect(connection: ChannelConnection): Promise<void>
  disconnect(): Promise<void>
  ready(): Promise<any>
  readonly connected: boolean
}

const checkListener = (listener: any) => {
  if (typeof listener !== 'function') {
    throw new TypeError(
      'The "listener" argument must be of type Function. Received type ' + typeof listener
    )
  }
}

const checkHandler = (handler: any) => {
  if (typeof handler !== 'function') {
    throw new TypeError(
      'The "handler" argument must be of type Function. Received type ' + typeof handler
    )
  }
}

export const RemoteChannelEmitter: new <
  Local extends EventsMap = DefaultEventsMap,
  Remote extends EventsMap = DefaultEventsMap
>(
  connection?: ChannelConnection
) => RemoteChannelEmitter<Local, Remote> = class<
  Local extends EventsMap = DefaultEventsMap,
  Remote extends EventsMap = DefaultEventsMap
> implements RemoteChannelEmitter<Local, Remote>
{
  private _i: Record<string, [(arg: any) => void, (args: any) => void]> = Object.create(null)
  private _h: Record<string, Function> = Object.create(null)
  private _l: Record<string, Function[]> = Object.create(null)
  private _c?: ChannelConnection
  private _id = 0
  private _p = Promise.resolve()

  constructor(connection?: ChannelConnection) {
    if (connection) {
      this.connect(connection)
    }
  }
  addListener<N extends EventNames<Omit<Local, keyof ReservedEventsMap> & ReservedEventsMap>>(
    name: N,
    listener: ChannelListener<this, Omit<Local, keyof ReservedEventsMap> & ReservedEventsMap, N>
  ): this {
    return this.on(name, listener)
  }
  on<N extends EventNames<Omit<Local, keyof ReservedEventsMap> & ReservedEventsMap>>(
    name: N,
    listener: ChannelListener<this, Omit<Local, keyof ReservedEventsMap> & ReservedEventsMap, N>
  ): this {
    checkListener(listener)
    const listeners = this._l[name] ?? (this._l[name] = [])
    listeners.push(listener)

    return this
  }
  once<N extends EventNames<Omit<Local, keyof ReservedEventsMap> & ReservedEventsMap>>(
    name: N,
    listener: ChannelListener<this, Omit<Local, keyof ReservedEventsMap> & ReservedEventsMap, N>
  ): this {
    checkListener(listener)
    const listeners = this._l[name] ?? (this._l[name] = [])
    listeners.push(getOnceListener(listener))

    return this
  }
  removeListener<N extends EventNames<Omit<Local, keyof ReservedEventsMap> & ReservedEventsMap>>(
    name: N,
    listener: ChannelListener<this, Omit<Local, keyof ReservedEventsMap> & ReservedEventsMap, N>
  ): this {
    return this.off(name, listener)
  }
  off<N extends EventNames<Omit<Local, keyof ReservedEventsMap> & ReservedEventsMap>>(
    name: N,
    listener: ChannelListener<this, Omit<Local, keyof ReservedEventsMap> & ReservedEventsMap, N>
  ): this {
    const listeners = this._l[name]
    if (listeners) {
      checkListener(listener)

      let idx = listeners.indexOf(listener)
      if (idx < 0) {
        idx = listeners.findIndex((l: any) => l[Function_Attr_Raw] === listener)
      }
      if (!(idx < 0)) {
        listeners.splice(idx, 1)
      }
    }
    if (listeners.length === 0) {
      delete this._l[name]
    }

    return this
  }
  prependListener<N extends EventNames<Omit<Local, keyof ReservedEventsMap> & ReservedEventsMap>>(
    name: N,
    listener: ChannelListener<this, Omit<Local, keyof ReservedEventsMap> & ReservedEventsMap, N>
  ): this {
    checkListener(listener)
    const listeners = this._l[name] ?? (this._l[name] = [])
    listeners.unshift(listener)

    return this
  }
  prependOnceListener<
    N extends EventNames<Omit<Local, keyof ReservedEventsMap> & ReservedEventsMap>
  >(
    name: N,
    listener: ChannelListener<this, Omit<Local, keyof ReservedEventsMap> & ReservedEventsMap, N>
  ): this {
    checkListener(listener)
    const listeners = this._l[name] ?? (this._l[name] = [])
    listeners.unshift(getOnceListener(listener))

    return this
  }
  removeAllListeners(name?: any): this {
    if (name != null) {
      delete this._l[name]
    } else {
      this._l = Object.create(null)
    }
    return this
  }
  listeners<N extends EventNames<Omit<Local, keyof ReservedEventsMap> & ReservedEventsMap>>(
    name: N
  ): ChannelListener<this, Omit<Local, keyof ReservedEventsMap> & ReservedEventsMap, N>[] {
    const listeners = this._l[name] as any[]
    return listeners ? [...listeners] : []
  }
  rawListeners<N extends EventNames<Omit<Local, keyof ReservedEventsMap> & ReservedEventsMap>>(
    name: N
  ): ChannelListener<this, Omit<Local, keyof ReservedEventsMap> & ReservedEventsMap, N>[] {
    const listeners = this._l[name] as any[]
    return listeners ? listeners.map((l) => l[Function_Attr_Raw] ?? l) : []
  }
  listenerCount<N extends EventNames<Omit<Local, keyof ReservedEventsMap> & ReservedEventsMap>>(
    name: N
  ): number {
    return this._l[name]?.length ?? 0
  }
  eventNames(): keyof Local[] {
    return Object.keys(this._l) as any
  }
  handle(name: string, handler: Function): any {
    checkHandler(handler)
    this._h[name] = handler
    return this
  }
  removeHandler(name: string): any {
    delete this._h[name]
    return this
  }
  getHandler(name: string) {
    return this._h[name] as any
  }
  removeAllHandlers(): any {
    this._h = Object.create(null)
    return this
  }
  handlerNames() {
    return Object.keys(this._h) as any
  }
  invoke(name: string, ...args: any[]) {
    return this[Field4](name, args, true) as any
  }
  connect(connection: ChannelConnection) {
    return (this._p = this._p.then(async () => {
      await this._disconnect()
      if (connection.disconnected) {
        throw new Error('ChannelConnection is disconnected.')
      }
      if ((connection as any)[Field1]) {
        if ((connection as any)[Field1] === this) return
        throw new Error('ChannelConnection is used by another instance of RemoteChannelEmitter.')
      }
      ;(connection as any)[Field1] = this
      this._c = connection
      await connection.whenConnected()
      this[Field2]('connected', [connection])
    }))
  }
  private async _disconnect(noRecursion?: any) {
    const { _c: connection } = this
    if (connection) {
      if (connection.disconnected) {
        ;(connection as any)[Field1] = undefined
        this._c = undefined
      } else {
        if (noRecursion !== NoRecursion_Flag) {
          this[Field4]('disconnected', [])
          connection.close()
        }
        this._c = undefined
        ;(connection as any)[Field1] = undefined
        if (noRecursion !== NoRecursion_Flag) {
          await connection.whenDisconnected()
          this[Field2]('disconnected', [connection])
        }
      }
    }
  }
  disconnect(noRecursion?: any) {
    return (this._p = this._p
      .then(() => this._disconnect(noRecursion))
      .catch(noop))
  }
  ready() {
    return this._p
  }
  get connected() {
    return !!this._c?.connected
  }
  [Field2](name: string, args?: any[]) {
    let listeners: any[] = this._l[name]
    if (listeners?.length > 0) {
      const removes = new Set<any>()
      try {
        for (const listener of listeners) {
          if (listener[Function_Attr_Once]) {
            removes.add(listener)
          }
          listener.apply(this, args)
        }
      } catch (e) {
        console.error(e)
      } finally {
        if (removes.size > 0) {
          listeners = listeners.filter((listener) => !removes.has(listener))
        }
        if (listeners.length > 0) {
          this._l[name] = listeners
        } else {
          delete this._l[name]
        }
      }
    }
  }
  [Field3](obj: any) {
    if (!Array.isArray(obj)) return
    try {
      const { name, id, type, args } = unwrapEventPacket(obj)
      if (name === '*') return
      switch (type) {
        case EventType.Emit:
          if (name === 'disconnected') {
            this.disconnect()
          } else {
            this[Field2](name, [new ChannelEvent(name, args)])
          }
          break

        case EventType.HandleSuccess:
        case EventType.HandleFailure:
          this[Field5](name, id, args, type)
          break

        case EventType.Invoke:
          this[Field6](name, id, args)
          break
      }
    } catch {}
  }
  [Field4](name: string, args: any[] = [], invoke?: boolean) {
    const { _c: connection } = this

    if (!connection || !connection.connected) {
      if (name === 'disconnected') return
      throw new Error('This emitter is disconnected.')
    }
    if (name === '*') return
    if (invoke) {
      const id = ++this._id
      return new Promise<any>((res, rej) => {
        this._i[invokeKey(name, id)] = [res, rej]
        const { _c } = this
        if (_c === connection && connection.connected) {
          connection._sendObject(wrapEventPacket(name, EventType.Invoke, id, args))
        }
      })
    } else {
      connection._sendObject(wrapEventPacket(name, EventType.Emit, 0, args))
    }
  }
  private [Field5](
    name: string,
    id: number,
    args: any[],
    type: EventType.HandleSuccess | EventType.HandleFailure
  ) {
    const key = invokeKey(name, id)
    const res = this._i[key]
    delete this._i[key]
    if (res) {
      res[type === EventType.HandleSuccess ? 0 : 1](args[0])
    }
  }
  private [Field6](name: string, id: number, args: any[]) {
    const { _c: connection } = this
    if (!connection?.connected) return
    const handler = this._h[name]
    let result
    if (handler) {
      result = Promise.resolve(handler.apply(this, args))
    } else {
      const commonHandler = this._h['*']
      if (commonHandler) {
        args.unshift(name)
        result = Promise.resolve(commonHandler.apply(this, args))
      } else {
        result = Promise.reject(new Error(`No handler named '${name}' is registered.`))
      }
    }

    const eventArgs = [new ChannelEvent(name, args, result)]
    if (!reservedEvents.has(name)) {
      this[Field2](name, eventArgs)
    }
    this[Field2]('*', eventArgs)

    return result.then(
      (...args) => {
        const { _c } = this
        if (_c === connection && connection.connected) {
          connection._sendObject(wrapEventPacket(name, EventType.HandleSuccess, id, args))
        }
      },
      (...args: any[]) => {
        const { _c } = this
        if (_c === connection && connection.connected) {
          connection._sendObject(wrapEventPacket(name, EventType.HandleFailure, id, args))
          connection._sendObject(wrapEventPacket('error', EventType.Emit, 0, args))
        }
      }
    )
  }
} as any

export default RemoteChannelEmitter
